package vivace.view;

/*
 * The skeleton for this code was excerpted from an example by Sun, and then
 * highly modified for this application. The could probably will be modified even
 * more in the future. The license is included below.
 * 
 * Copyright (c) 1999 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use,
 * modify and redistribute this software in source and binary code form,
 * provided that i) this copyright notice and license appear on all copies of
 * the software; and ii) Licensee does not utilize the software in a manner
 * which is disparaging to Sun.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT
 * BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING,
 * MODIFYING OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL
 * SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR
 * DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES,
 * HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF
 * THE USE OF OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGES.
 *
 * This software is not designed or intended for use in on-line control of
 * aircraft, air traffic, aircraft navigation or aircraft communications; or
 * in the design, construction, operation or maintenance of any nuclear
 * facility. Licensee represents and warrants that it will not use or
 * redistribute the Software for such purposes.
 */


import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.NoSuchElementException;
import java.util.Observable;
import java.util.Observer;
import java.util.Vector;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;
import javax.swing.JPanel;

import vivace.exception.NoChannelAssignedException;
import vivace.model.Action;
import vivace.model.App;

public class Keyboard extends JPanel implements Observer
{

	private static final long serialVersionUID = 4074154835728075879L;

	// Constants
	public final static Color KEY_PRESSED_COLOR = new Color (204, 204, 255);
	public final static int ORIENTATION_HORIZONTAL = 1;
	public final static int ORIENTATION_VERTICAL = 2;
	public static enum KeyColor{ WHITE,BLACK };
	public final static int WHITE_KEY_HEIGHT = 60, WHITE_KEY_WIDTH = 16;
	public final static int BLACK_KEY_HEIGHT = 30, BLACK_KEY_WIDTH = 8;
	
	/**
	 * The height of a note in the piano roll. Applies to the midi events in the Piano Roll as
	 * well as the bars in the Track Content view.
	 */
	public final static int BIGNOTE_HEIGHT = WHITE_KEY_WIDTH - BLACK_KEY_WIDTH/2;
	
	/**
	 * The height of a note in the piano roll. Applies to the midi events in the Piano Roll as
	 * well as the bars in the Track Content view.
	 */
	public final static int SMALLNOTE_HEIGHT = BLACK_KEY_WIDTH;

	
	// Private fields
	private int orientation;
	private Key theKey;
	private MidiChannel channel;
	private int channel_no;
	private Receiver rcv;
	private int lowNote,highNote;
	private Vector<Key> keys = new Vector<Key>();
	private Vector<Key> blackKeys = new Vector<Key>();
	private Vector<Key> whiteKeys = new Vector<Key>();

	/**
	 * Constructor
	 * @param lowNote The lowest note on the keyboard
	 * @param highNote The highest note on the keyboard
	 * @param orientation The orientation
	 */
	public Keyboard( int lowNote, int highNote, int orientation ){
		this.lowNote = lowNote;
		this.highNote = highNote;
		this.orientation = orientation;
		initialize();
		addMouseListener( new KeyMouseListener() );
		setBackground( Color.WHITE );
	}

	/**
	 * Empty constructor that creates a horisontal keyboard spanning from C1 to C7
	 */	
	public Keyboard() {
		this(24,96,Keyboard.ORIENTATION_HORIZONTAL);	// default to 6 octaves from C1 to C7
	}
	
	// Common intitialization
	private void initialize(){

		if (App.hasProjects()) {
	
			// Tell the model to notify about UI updates
			App.addProjectObserver(this, App.Source.ALL);
			
			// Set the channel and the receiver
			setChannel();
			setReceiver();
			
		} else {
			
			channel_no = 0;
			try {
				channel = MidiSystem.getSynthesizer().getChannels()[channel_no];
			} catch (MidiUnavailableException e) { }
		}

		// Variables for the preferred dimension
		int width = 0, height = 0;
		
		// Fill the key arrays
		if( orientation == ORIENTATION_HORIZONTAL ){
			
			// Counter for the y-coordinate 
			int currentX = 0;
			Key key;
			// Iterate over the notes and add them to the lists
			for( int i = lowNote; i <= highNote; i++ ){
				
				switch( i % 12 ){
				case 1: case 3: case 6: case 8: case 10:
					// The current note is a black key
					key = new Key((currentX - BLACK_KEY_WIDTH/2), 0, BLACK_KEY_WIDTH, BLACK_KEY_HEIGHT, i, KeyColor.BLACK);
					blackKeys.add (key);
					break;
				default:
					// The current note is a white key
					key = new Key(currentX,0, WHITE_KEY_WIDTH, WHITE_KEY_HEIGHT, i, KeyColor.WHITE);
					whiteKeys.add(key);
					currentX += WHITE_KEY_WIDTH;
					break;
				}
				keys.add(key);
			}
			
			width = currentX;
			height = WHITE_KEY_HEIGHT;
			
		} else if( orientation == ORIENTATION_VERTICAL ){

			// Counter for the y-coordinate 
			int currentY = 0;
			Key key;
			
			// Iterate backwards over the notes and add them to the lists
			for( int i = highNote; i >= lowNote; i-- ){
				
				switch( i % 12 ){
				case 1: case 3: case 6: case 8: case 10:
					// The current note is a black key
					key = new Key (0, (currentY - BLACK_KEY_WIDTH/2), BLACK_KEY_HEIGHT, BLACK_KEY_WIDTH, i, KeyColor.BLACK);
					blackKeys.add(key);
					break;
				default:
					// The current note is a white key
					key = new Key(0, currentY, WHITE_KEY_HEIGHT, WHITE_KEY_WIDTH, i, KeyColor.WHITE);
					whiteKeys.add(key);
					currentY += WHITE_KEY_WIDTH;
					break;
				}
				keys.add(key);
			}
			
			width = WHITE_KEY_HEIGHT;
			height = currentY;
			
		}
		
		// Set the preferred size
		setPreferredSize( new Dimension( width, height ));
		
	}

	// Assigns the receiver to the keyboard
	private void setReceiver(){
		try {
			rcv = App.Project.getSequencer().getReceiver();
		} catch (MidiUnavailableException e) {
			e.printStackTrace();
		}	
	}
	
	// Assigns the channel to the keyboard
	private void setChannel() {
		
		try {
			int t = App.UI.getTrackSelection().iterator().next();
			channel_no = App.Project.getTrackChannel(t);
		} catch (NoSuchElementException e) {
			channel_no = 0;
		} catch (NoChannelAssignedException e) {
			channel_no = 0;
		}
		
		channel = App.Project.getSynthesizer().getChannels()[channel_no];
	}
	
	@Override
	public void update(Observable o, Object arg) {
		
		if( arg instanceof Action ){
			
			Action a = (Action) arg;
			switch (a) {
				case TRACK_SELECTION_CHANGED:
					setChannel();
					break;
				case DEVICE_CHANGED:
					setReceiver();
					if( DumpReceiver.getInstance() != null ) {
						DumpReceiver.getInstance().deleteObserver(this);
						DumpReceiver.getInstance().addObserver(this);
					}
					break;
			}
			
		} else if ( arg instanceof ShortMessage ){
			ShortMessage sm = (ShortMessage) arg;
			
			if( sm.getData1() < 0 || sm.getData1() > 127 ){
				return;
			} else if( sm.getCommand() == ShortMessage.NOTE_ON && sm.getData2() > 0  ){
				keys.get(127-sm.getData1()).on();
				repaint();
			} else if( sm.getCommand() == ShortMessage.NOTE_OFF || sm.getCommand() == ShortMessage.NOTE_ON && sm.getData2() == 0 ){
				keys.get(127-sm.getData1()).off();
				repaint();
			}
			
		}

	}

	
	@Override
	public void paintComponent( Graphics g ){
		
		Graphics2D g2 = (Graphics2D) g;
		g2.setColor(Color.WHITE);
		g2.fill( new Rectangle(0,0, getPreferredSize().width, getPreferredSize().height) );
		
		/* Paint the white keys first to give the illusion that they are underneath the black ones */
		for( Key key : whiteKeys ){

			if( key.isNoteOn() ){
				g2.setColor( KEY_PRESSED_COLOR );
				g2.fill(key);
			}
			g2.setColor(Color.BLACK);
			g2.draw(key);

		}
		
		/* Then paint the black ones */
		for( Key key : blackKeys ){
			if( key.isNoteOn() ) {
				g2.setColor( KEY_PRESSED_COLOR );
			} else {
				g2.setColor( Color.BLACK );
			}
			g2.fill(key);
			g2.setColor(Color.BLACK);
			g2.draw(key);
			
			// Draw a white "light reflection" line
			g2.setColor(Color.WHITE);
			if( orientation == ORIENTATION_HORIZONTAL ) {
				g2.fillRect( key.x + 2, BLACK_KEY_HEIGHT - 2, BLACK_KEY_WIDTH - 2, 1 );
			} else {
				g2.fillRect( BLACK_KEY_HEIGHT - 2, key.y + 2, 1, BLACK_KEY_WIDTH - 2 );
			}      
			
		}
		
	}

	private class KeyMouseListener extends MouseAdapter {
		
		@Override
		public void mousePressed (MouseEvent e){
			theKey = getKey(e.getPoint());
			if (theKey != null){
				theKey.on ();
				repaint();
			}
		}

		@Override
		public void mouseReleased (MouseEvent e){
			if (theKey != null){
				theKey.off ();
				repaint ();
			}
		}

		@Override
		public void mouseExited (MouseEvent e){
			// This method is called if the mouse is moved
			// off the keyboard component. If a key was
			// pressed, we release that key.
			if (theKey != null){
				theKey.off ();
				repaint ();

				// The following assignment is needed so
				// that we don't execute the code within
				// mouseReleased()'s if statement should we
				// release a key after exiting the keyboard 
				// component. There is no need to tell the
				// key to stop playing a note after we have
				// already told it to do so. Furthermore,
				// we prevent an unnecessary repaint.
				theKey = null;
			}
		}
		
		/**
		 * Find the key with the highest z-value on the specified point
		 * @param point
		 * @return
		 */
		public Key getKey (Point point){
			Vector<Key> tmp = new Vector<Key>();
			tmp.addAll(blackKeys); tmp.addAll(whiteKeys); // Note: It looks stupid, but we want it this way ;)
			for( Key key : tmp ) {
				if (key.contains (point)) {
					return key;
				}
			}
			return null;
		}

	}
	

	/**
	 * Private class modelling a keyboard key, with information
	 * about its state (ON/OFF), it's color (WHITE/BLACK) and it's note value
	 */
	private class Key extends Rectangle 
	{
		private static final long serialVersionUID = -2263234943956943170L;

		// constant fields
		private final static int ON = 0, OFF = 1, VELOCITY = 127;
		
		private KeyColor keyColor;
		private int note;
		private int state = OFF;

		/**
		 * Constructor
		 * @param x
		 * @param y
		 * @param width
		 * @param height
		 * @param note
		 * @param keyColor
		 */
		public Key (int x, int y, int width, int height, int note, KeyColor keyColor ){
			super (x, y, width, height);
			this.note = note;
			this.keyColor = keyColor;
		}


		/**
		 * Returns whether or not the key is in ON-state
		 * @return
		 */
		public boolean isNoteOn(){
			return state == ON;
		}

		/**
		 * Returns whether or not the key is a white key
		 * @return
		 */
		public boolean isWhiteKey(){
			return keyColor == KeyColor.WHITE;
		}
		
		/**
		 * Sends a NOTE_OFF event to the receiver
		 * @return
		 */
		public void off (){
			
			setState (OFF);
			if (channel != null) {
				channel.noteOff (note, VELOCITY);
			}

			ShortMessage sm = new ShortMessage();
			
			try {
				sm.setMessage(ShortMessage.NOTE_OFF, channel_no, note, VELOCITY);
			} catch (InvalidMidiDataException e) {
				e.printStackTrace();
			}
			
			if (App.hasProjects()) {
				rcv.send(sm, -1);
			}
			
		}

		/**
		 * Sends a NOTE_ON message to the receiver
		 */
		public void on (){
			
			setState (ON);
			if (channel != null) {
				channel.noteOn (note, VELOCITY);
			}
			
			ShortMessage sm = new ShortMessage();
			
			try {
				sm.setMessage(ShortMessage.NOTE_ON, channel_no, note, VELOCITY);
			} catch (InvalidMidiDataException e) {
				e.printStackTrace();
			}
			
			if (App.hasProjects()) {
				rcv.send(sm, -1);
			}

		}

		/**
		 * Sets the state of the key
		 * @param state
		 */
		public void setState (int state){
			this.state = state;
		}
		
	}
	
	
} 
